尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

GEE中Sentinel-2影像加载、去云与投影校正实战指南

GEE中Sentinel-2影像加载、去云与投影校正实战指南 做遥感的朋友应该都有同感Sentinel-2在GEE里用得有多频繁踩坑就有多容易。光一个“加载影像、去云、再算个NDVI”的流程群里已经有好几个人拿着诡异的条纹图、错位图来问我了。仔细一看十有八九是栽在影像集合选错、云掩膜过滤过猛、还有投影不一致这三个地方。这篇就把S2在GEE里的影像加载、三套去云方案、以及投影变化对后续计算NDVI等指数的影响一次讲清楚全程给代码、给参数、给判断依据照着抄能少走很多弯路。1. S2影像集合选型别一上来就 ee.ImageCollection(COPERNICUS/S2)很多新手教程喜欢直接用COPERNICUS/S2但实际做定量分析、算NDVI序列的时候我强烈建议先分清楚自己需要的是L1C还是L2A。这两个数据集在GEE里是完全不同的入口选了哪个直接决定了你后续做不做大气校正、去云的时候看哪个波段。1.1 L1C、L2A和SR_HARMONIZED的区别COPERNICUS/S2是L1C产品存的是大气表观反射率TOA。它最大的问题是没有做大气校正水汽、气溶胶的影响全都在里面直接拿它算NDVI植被区域还行但到了多云多雾或者地形复杂的区域数值会明显失真。COPERNICUS/S2_SR是L2A产品存的是地表反射率SR。这是经过大气校正的要算NDVI、NDWI、叶面积指数这些定量产品首选就是它。COPERNICUS/S2_SR_HARMONIZED是GEE官方后来推出的“调和版”L2A。它的出现是因为哨兵2A和2B两颗卫星在2022年1月之后出现了辐射定标偏移导致新旧影像同一地物的反射率数值对不上。如果直接混用旧数据和新数据做时间序列会在2022年初出现一个肉眼可见的“台阶”。HARMONIZED版本把2022年前的数据统一调整到了新的定标尺度上所以做长时序分析时务必用这个集合。一句话总结常规练习用哪个都行但要做时间序列、要发文章、要和其他年份对比优先COPERNICUS/S2_SR_HARMONIZED。这里也提一下COPERNICUS/S2_SR和 HARMONIZED 在波段结构上完全一致都是B1到B12外加QA10、QA20、QA60和SCL波段所以下面的代码在两个集合上都能跑。1.2 加载S2影像集合时的常见参数加载影像集合不是简单一句from就完事。我一般会按这个顺序做筛选逻辑清晰计算量也小先按时间过滤把研究时段卡死再按研究区filterBounds裁剪到目标范围别让全世界的影像都参与后续计算然后按云量过滤用的是元数据里的CLOUDY_PIXEL_PERCENTAGE新手最容易漏掉这一步加载之后先first()或者mosaic()看一眼确认投影、波段、云覆盖情况再往下走。var s2 ee.ImageCollection(COPERNICUS/S2_SR_HARMONIZED) .filterDate(2023-01-01, 2023-12-31) .filterBounds(roi) .filter(ee.Filter.lt(CLOUDY_PIXEL_PERCENTAGE, 30));注意CLOUDY_PIXEL_PERCENTAGE这个元数据是GEE根据算法预先算好的影像整体云量它不等于你研究区内的真实云量。如果roi只是影像里很小的一块哪怕这块是晴天整景影像云量是80%照样会被过滤掉。反过来也一样研究区里云很重整景云量只有5%。所以做小区域分析时别把阈值卡太死宁可进来之后再局部做云掩膜。2. 三套S2去云方案的原理、代码与选型对比去云这个话题GEE里聊得最多但真正能稳定落地的方案其实就那几套。我在实际项目中反复对比过把最常用的三套整理出来QA60位掩膜、SCL场景分类、多条件组合去云。每套都有适用的场景也有各自的坑。2.1 方案一基于QA60波段的经典位掩膜去云S2 L2A产品的QA60波段是一个按位编码的质量波段第10位表示云第11位表示卷云。所谓“位掩膜”就是把像素值的二进制位拆开来看对应位置的0/1状态。GEE里没有直接“读位”的函数要用bitwiseAnd配合位移运算来做。function maskS2clouds(image) { var qa image.select(QA60); // 第10位是云第11位是卷云 var cloudBitMask 1 10; var cirrusBitMask 1 11; var mask qa.bitwiseAnd(cloudBitMask).eq(0) .and(qa.bitwiseAnd(cirrusBitMask).eq(0)); return image.updateMask(mask); } var s2Masked s2.map(maskS2clouds);这里有一个值得解释的点为什么用1 10而不是直接写1024位移表达式的可读性更高别人看你代码时能一眼明白你是在“制造一个只有第10位是1的二进制数”。GEE里bitwiseAnd相当于按位“与”运算结果不等于0说明这一位是1表示有云等于0说明这一位是0表示没有云。两段判断用.and()接起来表示“云和卷云都没有”的像素才保留。这套方案最大的优点是快、通用、代码量少。缺点是QA60对薄云和云阴影基本无能为力而且S2 L2A里有些薄云区域QA60标的是0但肉眼看上去还是白蒙蒙一片。所以它适合做快速浏览、大范围制图不适合对精度要求很高的定量反演。2.2 方案二基于SCL波段做场景分类去云SCL波段是L2A产品自带的场景分类结果每个像素值代表一种地物类别。类别编码含义如下值类别值类别0无数据6水体1饱和/缺陷7低概率云/未分类2暗影8中概率云3云阴影9高概率云4植被10卷云5裸土11雪/冰去云的核心思路就是把值为3云阴影、8、9、10的像素全部去掉视情况保留2暗影。注意暗影和云阴影不同暗影可能是地形阴影不一定是云造成的加不加由你的研究区决定。function sclCloudMask(image) { var scl image.select(SCL); var cloudMask scl.neq(3) .and(scl.neq(8)) .and(scl.neq(9)) .and(scl.neq(10)); return image.updateMask(cloudMask); }需要特别留意的是SCL里还有一个“低概率云/未分类”是7。这个类别在不同场景下表现得极其不稳定有些开阔平地上它也标7所以做农田、裸土区域反演时常被误杀建议根据自己的研究区试跑一两次再决定要不要把7也滤掉。SCL方案相比QA60的优势在于能识别云阴影这在大范围山地、城市高楼区域非常重要。缺点是误分类率不低尤其在城市和复杂地表环境下建筑物阴影很容易被分到云阴影那一类导致把好像素给剔除了。2.3 方案三QA60加SCL加光谱阈值组合去云第三套方案是项目实战中最常用的也是我目前给团队内部定的标准方案。思路是取前两者的交集再加一个基于亮度波段的薄云检测尽可能把厚云、薄云、云阴影一次性清干净function combinedCloudMask(image) { var qa image.select(QA60); var qaMask qa.bitwiseAnd(1 10).eq(0) .and(qa.bitwiseAnd(1 11).eq(0)); var scl image.select(SCL); var sclMask scl.neq(3) .and(scl.neq(8)) .and(scl.neq(9)) .and(scl.neq(10)); var bright image.select(B2).add(image.select(B3)).add(image.select(B4)); var brightMask bright.lt(1500); // 经验阈值单位是反射率*10000 return image.updateMask(qaMask.and(sclMask).and(brightMask)); }这里B2 B3 B4是蓝绿红三个可见光波段的和云在这三个波段反射率极高尤其蓝波段。取1500这个阈值是我在多个项目里试出来比较稳妥的经验值大约对应地表反射率0.15。不同地区、不同太阳高度角会有差别需要自己调。这套组合的问题也很明显阈值部分需要反复验证不适合一上来就无脑套到所有地区。三套方案可以这么选不看精度、只出图用方案一研究区地形简单、主要是云阴影影响用方案二像我这样要拿影像做后续NDVI时序分析、要保证数据质量统一性的用方案三。3. 投影变化为什么你算出来的NDVI边缘有锯齿这一节是整个GEE S2处理中新手最容易忽略的关卡。S2原始产品的投影并不是简单的WGS84经纬度而是分带的UTM投影。每景影像根据所处的UTM分带不同投影参数也不同这就导致你把两景相邻影像拼起来时它们的投影可能不一样像素中心对不上后续算出来的NDVI就会出现错位、重影和锯齿边缘。3.1 GEE里的投影到底是怎么变化的先看怎么获取一景影像的真实投影信息var image s2.first(); print(投影信息, image.projection()); print(B8波段投影, image.select(B8).projection());在控制台你会看到类似EPSG:32650这样的代码这就是UTM 50N分带。GEE在显示影像时默认会做动态重投影把影像画到屏幕的坐标系下所以你肉眼看不出来有问题。但当你做波段运算时运算结果会继承第一个输入波段的投影。换句话说如果你把B810米和B1120米放在一个表达式里算NDBI输出的结果投影可能跟着B8走也可能跟着B11走具体看谁的“优先级”高这种不确定性就是后面各种怪图的根源。要特别强调的是NDVI本身用的是B8和B4两个波段分辨率都是10米所以直接算NDVI其实很少出现投影错位。但如果你像我一样习惯把NDVI、NDBI、NDWI、EVI等好几个指数放在一个expression里一次性算完那NDBI里B8和B11的不同分辨率问题就会牵扯到整张结果图导致NDVI边缘也跟着出现锯齿。3.2 统一分辨率和投影的两种做法第一种做法是重采样。把20米波段通过resample(bilinear)插值到10米然后再参与计算var b8 image.select(B8).resample(bilinear); var b11 image.select(B11).resample(bilinear); var ndbi b11.subtract(b8).divide(b11.add(b8)).rename(NDBI);第二种做法是用reduceResolution做空间聚合。它的逻辑不是插值而是把每个输出像素内覆盖的原始像素做统计比如取均值这样更符合物理意义。代码是这样的var b11_10m image.select(B11) .reduceResolution({ reducer: ee.Reducer.mean(), bestEffort: true, maxPixels: 1024 }) .reproject({ crs: image.select(B8).projection() });这两个方案我都用过。说实话日常快速分析用重采样就够了代码简单速度也快。但要发文章、要做非常精确的城市热岛或者生物量估算就建议用reduceResolution它对像元能量的保真度更好不会凭空插值出原分辨率上不存在的细节。3.3 导出时投影设置不对结果全白搭投影问题不只是屏幕上看着别扭导出时才是重灾区。有人用GEE导出一幅NDVI在网页里看图挺正常拿回ArcGIS或者QGIS里一打开发现影像的角点坐标和研究区边界对不上或者整张图是斜的。这基本就是导出时没有指定投影坐标系导致的。我导出的标准配置一般都长这样Export.image.toDrive({ image: ndvi, description: ndvi_export, region: roi, scale: 10, crs: EPSG:32650, maxPixels: 1e13 });关键在crs这个参数。如果是中国中东部地区EPSG:32650或32651是常选的UTM分带如果研究区跨了两个分带那就老老实实先用roi.centroid()手动查一下中心点落在哪个分带再填对应的EPSG代码。注意scale和crs必须配套你指定了UTM投影再给scale: 10这10米才是地面真实距离10米如果不指定crsscale会被默认理解成在EPSG:4326下的10度分辨率导出来的图要么巨大无比要么变形严重。4. 实战全流程S2影像加载、去云到NDVI计算一次跑通前面每一部分都是单点突破这一节把它们串成一个完整流程。我以下面的场景为例给定一个矢量边界roi加载2023年6到8月、云量低于30%的S2 L2A影像用组合去云方案处理再计算NDVI最后导出GeoTIFF。这是一套我反复跑过很多遍的“基操”流程直接可用。4.1 加载与预处理从影像集合到单景影像var roi ee.FeatureCollection(users/your_username/roi); var startDate 2023-06-01; var endDate 2023-08-31; var s2 ee.ImageCollection(COPERNICUS/S2_SR_HARMONIZED) .filterDate(startDate, endDate) .filterBounds(roi) .filter(ee.Filter.lt(CLOUDY_PIXEL_PERCENTAGE, 30)); var s2Masked s2.map(combinedCloudMask); var composite s2Masked.median();这里我用了median()做合成原因是多景影像取中位数能进一步压制残留的薄云、阴影像素。相比mean()中位数对极端值不敏感做时间合成更稳。注意取中位数之前要确保云掩膜已经应用到位否则中位数会把云覆盖区的异常值也当成正常数据统计进去。4.2 计算NDVI时的波段选择与投影统一NDVI的公式是(NIR - Red) / (NIR Red)S2里对应B8和B4。这里有一个新手常犯的错误用B8A20米替代B810米。虽然B8A也属于近红外范畴但它的波谱范围和B8不同算出来的NDVI和Landsat的NDVI没有可比性。要做一致性比较老老实实用B8。var ndvi composite.normalizedDifference([B8, B4]).rename(NDVI); // 如果担心投影问题可以显式重投影到B8的投影上 var ndviReprojected ndvi.reproject({ crs: composite.select(B8).projection() });这里我想解释一下reproject的代价。GEE中所有计算都是“惰性”的只有在导出、可视化、或调用getInfo时才会真正执行。如果你在链条中间加一个reproject就相当于强制在这一个节点把数据按指定投影和分辨率算出来这一下会打断后面的金字塔优化让后续每一个操作都基于这个重投影后的固定分辨率。频繁使用reproject会显著拖慢运行速度真不是吓唬人。我的建议是计算时该重投影就重投影但别在中间结果上反复print和getThumbURL那会让重投影反复执行卡到怀疑人生。4.3 可视化和导出的细节可视化NDVI我习惯用调色板#d73027, #f46d43, #fdae61, #fee08b, #d9ef8b, #a6d96a, #66bd63, #1a9850范围设在-0.2到0.8之间这样能比较好地突出低植被和高植被的差异。Map.centerObject(roi, 10); Map.addLayer(ndvi, {min: -0.2, max: 0.8, palette: [#d73027, #f46d43, #fdae61, #fee08b, #d9ef8b, #a6d96a, #66bd63, #1a9850]}, NDVI);导出时除了前面说的crs和scale还有一个小坑region如果直接用矢量边界导出的像元范围会和矢量边界的包围盒对齐但边缘会出现半像元溢出。我一般会对roi做一个bounds()处理让它变成矩形再导出切出来的图边缘更规整。Export.image.toDrive({ image: ndvi, description: ndvi_2023_summer, region: roi.geometry().bounds(), scale: 10, crs: EPSG:32650, folder: GEE_exports, maxPixels: 1e13 });整套流程跑下来不会超过5分钟只要控制台不报错基本就能拿到能用的NDVI影像。即使不用来发文章这套流程也可以当一个“质量检查器”每次拿到新研究区先跑一遍这个流程快速确认影像覆盖、云量和投影情况比对着原始影像一张张翻大概率高效得多。5. 高频问题与排查技巧从投影错位到去云过度的处理实录写到这里把我在评论区、工作群和线下交流里被问得最多的几个问题集中拿出来说一说。有些问题看起来是代码报错实际是概念没拧清有些问题表面上是数据不对其实是投影或者去云逻辑埋了雷。5.1 问题一多时相NDVI叠加后出现“条带”或“棋盘格”表现每年同一天的NDVI影像单独看都正常把多年影像做均值合成或变化检测时出现规则条纹。排查思路先看每景影像的投影代码也就是print(image.projection())。如果相邻年份或相邻季节的影像来自不同UTM分带拼接边界就是条带的来源。解决方法是所有影像先reproject到研究区中心点对应的UTM投影再做后续合成。还有另外一种可能是云掩膜造成的空隙在合成时用了first()而非median()导致某些像素直接取了某一年云边缘的值周边又是晴空值看起来就像条带。5.2 问题二SCL去云后水体被整片删掉了表现用SCL方案做去云结果河面、水库区域呈现大片无数据。原因SCL在云阴影检测时经常把水体误判成云阴影值标为3。特别在山体阴影倒映在水面上时这种误判概率明显上升。解决方法是SCL掩膜里放宽对3的限制或者结合水体的光谱特征单独保护水体像元。我自己常用的做法是先计算一个NDWI把水体像元单独拎出来云掩膜不作用在水体上。5.3 问题三NDVI导出后全图都是负值或异常低值表现网页里看NDVI图有绿有红导出到本地打开后整张图数值都在-1附近。排查思路这通常不是算法问题而是拉伸显示问题。网页端GEE默认会做2%到98%的线性拉伸导出的GeoTIFF则保留原始数值。用ArcGIS打开时如果没有应用拉伸整张图看起来就是黑乎乎一片但你用识别工具点一下像元值就会发现数据本身没问题。解决办法是在GIS软件里设置拉伸方式为“最值拉伸”或“百分比截断拉伸”如果还是低那就检查是否把L1C的TOA反射率当成SR用了TOA的NDVI在水体和阴影区域确实会比SR更低。5.4 问题四GEE报错Image.reduceResolution: Too many pixels per tile这个报错在我初学阶段困扰了我很久字面意思是“每个瓦片里要处理的像素太多了”。实际原因是reduceResolution把低分辨率波段聚合到高分辨率时一个输出像素覆盖的原始像元数量超过了GEE内存限制。解决办法是给maxPixels一个合理值比如1024然后在reproject时指定输出金字塔级别别让GEE自己去猜。另外bestEffort: true也会自动调整聚合尺度来规避这个报错如果不追求极端精度开着它最省心。五节内容基本对应了“加载-去云-投影-计算-排错”的完整链路。我想特别强调一点投影问题在单景影像、单时相计算里可能完全看不出来但只要涉及时间序列、多景拼接或跨分带研究区就一定会爆发。这也是为什么我在团队里一直强制要求所有S2处理脚本的第一步必须是“打印投影确认分带统一坐标系”。别嫌这一步麻烦它真的是后面所有分析的地基。如果你还没养成这个习惯下次跑通流程之后建议先手动检查一下投影再决定要不要继续往下做。
返回列表