在Google Earth Engine里同时用Sentinel-1和Sentinel-2,最让人头疼的从来不是数据下载,而是怎么把这两类成像机制完全不同的影像,在时间上对齐、在空间上对准、然后把波段真正合并成一张“能干活”的复合影像。做农情监测、洪水制图、土地覆盖分类的朋友应该都有感触:光学影像再好,一到雨季就满屏是云;雷达影像不怕云,但是斑点噪声和几何特征又让你没法直接用它出漂亮的植被指数图。这篇博文就想聊聊怎么在GEE里把Sentinel-1的VV/VH极化通道和Sentinel-2的多光谱波段,通过时间窗口和波段堆叠,做成一个统一坐标、统一分辨率、统一时间标签的影像集。如果你是刚开始接触遥感数据融合的小白,或者已经会用GEE但一直被“波段合不上、时间对不齐”折磨,这篇文章应该能帮你省下不少试错时间。
1. 整体设计与思路拆解
1.1 为什么非要把Sentinel-1和Sentinel-2绑在一起
先搞清楚一个问题:光学影像和雷达影像到底互补在哪?
Sentinel-2是光学多光谱卫星,它记录的是地物对太阳辐射的反射信号,我在日常处理中几乎天天跟它的B2、B3、B4、B8这些波段打交道。这些波段组合起来能算NDVI、NDWI、EVI,能直接看出植被长得好不好、水体边界在哪里,视觉效果也跟人眼看到的接近,非常好解释。但它的致命弱点是,只要有云,地面信号就被挡死了,而且在热带、亚热带地区,一年里能拿到清晰影像的窗口可能只有那么几个月。
Sentinel-1是合成孔径雷达卫星,它主动发射微波并接收回波,不依赖太阳光照,更不惧云雨。VV极化和VH极化分别对地表的粗糙度、介电常数、结构特征敏感,尤其是在土壤湿度反演、洪水识别、水稻识别这些场景里,雷达数据几乎是不可替代的。但它的缺点也很明显:相干成像带来大量斑点噪声,图像看起来“麻点”很多;而且后向散射系数对地物类型的解释没有光学影像那么直观,非专业人员看了头大。
把两类影像放在一起做“连接分析”,本质上就是把两个互补的信息源拉进同一个特征空间。我举个实际例子:在做作物分类时,Sentinel-2的NDVI时间序列可以把不同作物区分开,但到了多云期,时间序列断了;Sentinel-1的后向散射时间序列虽然噪声大,但对作物结构非常敏感,能顶上空档期。把两个数据源的多波段合并成一个影像,再作为分类器的输入特征,分类精度往往比单用任何一类数据都高出一截。
这就是连接分析的底层逻辑:不是简单地把图片叠在一起看,而是让两种异质数据在时间维度上对齐、在空间维度上配准、在特征维度上互补。
1.2 连接分析与多波段合并的本质
标题里的“同一时间的多波段合并”,拆开来看其实是三件事。
第一件事是时间对齐。哨兵1号的重访周期和哨兵2号的重访周期并不是同步的。两颗Sentinel-2卫星配合起来每5天覆盖一次同一地点,Sentinel-1在赤道区域大概6天一重访,在高纬度更频繁。如果没有一套合理的时间匹配策略,直接把所有影像拿来合,要么数据缺失严重,要么把不同时相的地物变化也“平均”进去了。
第二件事是空间配准。Sentinel-1的GRD产品经过地形校正后,和Sentinel-2的L2A级产品大体都能对齐,但它们默认的CRS和像素网格不一样。GEE在计算时通常按需重采样,但如果你不显式统一投影和分辨率,导出的影像很容易出现边缘错位,尤其在山区。
第三件事才是真正的波段合并。把S1的VV、VH两个波段和S2的蓝、绿、红、近红、短波红外等波段,用addBands()叠成一个多波段的影像对象,后续就可以直接把它当输入特征集用了。
这三件事里,时间对齐是最核心的,它决定了合并出来的影像是否具有物理意义。后面你会看到,我习惯用两种方法做时间对齐:一种是以固定时间窗口做“阶段合成”,另一种是用GEE的Join机制做“最近邻匹配”。这两种方法各有适用场景,我都会在第四节给出完整代码。
1.3 适合什么场景,不适合什么场景
我见过不少人拿到S1+S2合并数据以后,发现效果并不理想,很多时候是因为场景没选对。
适合合并数据集的场景包括:多云区域的土地覆盖分类,比如热带雨林、东南亚稻田、云贵高原山区;洪水事件中的水体提取,雷达能穿透云层识别水体,光学波段能辅助区分浑浊水体;作物物候长时序分析,用合成序列补充光学缺口;以及森林扰动监测,S1的后向散射变化和S2的植被指数变化可以相互印证。
不太适合硬凑的场景包括:需要日内或逐日高时间分辨率的动态监测,因为合成窗口本身就会平滑掉快速变化;对极化分解有精细要求的雷达研究,比如要做Pauli分解、Freeman-Durden分解,那就不应该把S1和S2合并,而是要保持SAR数据的极化信息完整性;还有高精度几何测量场景,雷达的几何畸变和光学影像的投影差异很难彻底消除,绝不能指望合并完就能直接量地物高度。
说直白一点:S1+S2合并,目标是“分类和识别”,不是“精确测量”。想清楚这个定位,后面的处理思路就不会跑偏。
2. 核心细节解析与实操要点
2.1 Sentinel-1预处理:GRD、极化通道与dB转换
在GEE里取Sentinel-1数据,一般用的是COPERNICUS/S1_GRD这个数据集,它是已经过初步处理的GRD(Ground Range Detected)产品。GRD意味着振幅信息是强度检测后的结果,没有相位信息,因此不能做干涉处理,但用于后向散射分析已经足够。
我在筛选S1影像时,通常会加三个条件:轨道模式必须是IW,这是哨兵1号在陆地上的标准干涉宽幅模式,扫描带宽度约250公里,空间分辨率在距离向和方位向都能达到10米左右;极化必须同时包含VV和VH;时间范围和研究区边界必须匹配。
S1数据有一个必须要做的操作:把幅度值转换成以分贝(dB)为单位的后向散射系数。为什么要转dB?因为雷达后向散射系数的动态范围非常大,从很暗的平静水面到很亮的建筑区,可能跨越几十倍甚至上百倍,人眼和很多算法在这种尺度下难以捕捉细微差异。取对数以后,强信号和弱信号之间的差异被压到一个更适合分析和可视化的范围内。在GEE里,常规做法是img.select('VV').log10().multiply(10),如果你希望结果更符合物理辐射定标,还需要考虑入射角、以及GRD产品本身在GEE后台已经做过辐射定标,所以这里直接取对数即可。
还有一个很多人会忽略的点:S1 GRD的原始值是振幅(amplitude),不是功率(power)。如果官方文档明确说该波段是振幅,那么严格来说要先平方得到功率,再取对数,也就是20*log10(value)。但在GEE社区的实际操作里,直接把GRD振幅值按10*log10(value)处理得到的“dB”值也能用,因为很多应用只关心相对变化,不关心绝对辐射量。不过我在自己的流程里,会跟数据说明文档核对一下再决定用哪个系数。如果你想做跨时间、跨轨道的定量比较,这一步千万别省。
斑点噪声也是S1的一大痛点。合成孔径雷达的相干成像机制决定了原始影像会有大量颗粒感噪声,直接拿去和S2合并,分类器的输入特征信噪比会很差。我在前方高能场景下通常会对做时间合成,利用多时相中值来压制噪声;如果是单景滤波,可以用Refined Lee滤波。但要注意,Refined Lee滤波不是GEE内置的算法,需要自定义核函数和局域统计计算,代码量大一点,而且过度滤波会抹掉边缘细节。对大部分用户来说,用一个短时间窗口的median()合成做降噪,效率最高、效果也最稳。
2.2 Sentinel-2预处理:L2A、云掩膜与波段选择
Sentinel-2数据在GEE里有两个常用数据集:COPERNICUS/S2和COPERNICUS/S2_SR。前者是L1C级产品,提供的是大气顶部反射率(TOA);后者是L2A级产品,提供的是大气底部反射率(SR),已经做过大气校正。我在做S1+S2合并时,无一例外都选S2_SR,因为只有表面反射率才能在时间序列里和雷达后向散射一起被比较,否则大气状态的变化会被误当成地表变化。
S2的波段很多,但并不是所有波段都适合直接参与合并。B1(海岸气溶胶)和B10(卷云)主要是为了大气校正服务的,B9是水汽波段,这仨不太适合作为分类特征。真正常用的地表特征波段是10米分辨率的B2(蓝)、B3(绿)、B4(红)、B8(近红外),以及20米分辨率的B5、B6、B7、B8A(红边和宽近红外)、B11、B12(短波红外)。在我合入S1时,一般只保留B2、B3、B4、B8、B11、B12这六个波段,红边波段看任务需求决定要不要加。这样既有光学色彩信息,又有红边和短波红外,配合S1的VV、VH,总共8个波段,特征是够用的。
云掩膜是S2预处理里最不能偷懒的步骤。如果不把云和云影像元剔掉,合并后的影像一旦在云区留下异常高反射信号,那个区域在整个分类模型里就会被当成一个“类”,后果很糟。S2的L2A产品提供了两种常用掩膜方案:一种是用QA60波段做位运算,另一种是用SCL波段做场景分类。读者不必两套都用,理解一个就行。
让我把QA60的方法讲清楚:QA60是一个位掩码波段,它的第10位和第11位分别表示“云”和“卷云”。要把云位置提取出来,用bitwiseAnd()和一个值为1<<10或1<<11的整数做与运算,就能得到对应位的二进制掩膜。然后取反,再和影像做updateMask(),就把云档掉了。这套代码在GEE官方示例里很常见,我已经用了很多次,值得直接抄。
如果换成SCL波段,思路更直观:SCL是逐像元的地表分类标签,比如3是云影、7是未标记云、8是云底、9是雪冰、10是高云、11是薄卷云。只要用remap()或者一系列eq()、neq()条件组合,把这些值排除,剩下的像元就是可信地表信息。SCL的缺点是在某些版本的数据里对大雾和海面上的薄云识别不稳定,所以我一般两种方法都会跑一遍,肉眼对比一下哪个更干净。
2.3 时间匹配策略怎么定:窗口合成还是近邻连接
处理S1和S2的“同一时间”问题,我常用两套策略,第一套是“时间窗口合成法”,第二套是“最近邻连接法”。
时间窗口合成法的思路很简单:把整个研究期切成等间隔的时间窗口,比如每10天一个窗口,或者每个月一个窗口,然后对窗口内的S1取中值、对窗口内无云的S2取中值,最后把两个中值影像合并成一个复合影像。这样得到的是一个“阶段平均”的数据集,好处是噪声被大量压制,S1的斑点噪声在时间中值后明显减少,S2的云也透过掩膜加中值被彻底剔除。
代价是时间分辨率降低,而且窗口内的地物如果发生过快速变化,比如洪水涨落、作物收割,中值会把人地物变化抹成一个过渡状态。窗口选多大,决定了你要“时间保真度”还是“数据完整度”。
最近邻连接法走的是另一条路:对每一景S2影像,在它的拍摄日期前后寻找最接近的一景S1影像,然后把这景S1的波段直接加到S2上。这样得到的每个影像对应一个真实的时间点,而不是一个平均窗口,对于物候变化分析更精确。缺点是一旦搜索窗口内没有S1数据,你就会得到空值,而且你说的“同一时间”其实严格来说是“时间差最小”。
我在实际项目里,两种方法会搭配着用:做年度土地覆盖分类的时候,用窗口合成法;做作物物候分析或者长时序变化检测的时候,用最近邻连接法。两种方法在GEE里的代码实现我都会在下一章给出,敬请期待。
2.4 分辨率与投影统一:10米网格与重采样的陷阱
S1的GRD产品是10米分辨率,S2的光学波段有10米、20米、60米三种,B11、B12这两个短波红外波段就是20米。如果你直接把B11加到一个10米的影像里,GEE在导出或者计算时,会按照你需要输出的投影来重采样,但不同band的默认投影并不一致,有人会在加法运算时遇到“requested bands are at different resolutions”的错误。
我的经验是:在合并之前,给S2的20米波段做一个显式重采样。GEE里可以这样写:img.select('B11').resample('bilinear'),resample只是改变了重采样方式,并不会立刻改变像素网格,但当你把它通过reproject()指定到一个10米网格时,重采样就会发生。更省事的办法是,在Export.image.toDrive()导出时统一指定scale: 10和crs,让GEE在导出阶段一次性重采样。这个办法我一直用,简单有效。
但要提醒一句:reproject不要滥用。在GEE里,过度使用reproject会让你把数据强制钉在一个本地像素网格上,导致后续任何涉及区域统计或不同尺度computation的操作都变得非常慢,甚至直接超时。正确做法是让数据保留“懒重采样”状态,只在导出或明确需要统一CRS时再动手。
3. 实操过程与核心环节实现
3.1 数据筛选:区域、时间与极化通道
写代码的第一步,永远是数据筛选。先定义研究区。我这里用点坐标加缓冲区示范,你可以替换成自己实际用到的研究区边界:
var region = ee.Geometry.Point([123.45, 32.67]).buffer(8000); var startDate = '2023-06-01'; var endDate = '2023-09-30';这段代码的意思是,在以该点为中心、半径8公里的范围内做分析,时间从2023年6月到9月底。缓冲区做个8000米,是为了让中值和合成计算有足够的样本量,不至于边缘像元太少。
接着筛选Sentinel-1:
var s1Col = ee.ImageCollection('COPERNICUS/S1_GRD') .filterBounds(region) .filterDate(startDate, endDate) .filter(ee.Filter.eq('instrumentMode', 'IW')) .filter(ee.Filter.listContains('transmitterReceiverPolarisation', 'VV')) .filter(ee.Filter.listContains('transmitterReceiverPolarisation', 'VH'));这串链式筛选,前两个是通用条件,后三个是用来保证每景影像都有IW模式和VV、VH两种极化。之所以要加极化筛选,是因为有些S1影像只有单极化,或者用的是波束模式而非IW,如果不筛掉,后面取波段时会报错或产生空值。
筛选Sentinel-2,我习惯再多加一个云量过滤条件:
var s2Col = ee.ImageCollection('COPERNICUS/S2_SR') .filterBounds(region) .filterDate(startDate, endDate) .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20)) .map(maskS2clouds);先把影像级别的云量控制在20%以下,再用像素级掩膜去精细处理。这两个条件不冲突:影像级筛选是为了少算一些无用数据,像素级掩膜是为了保证参与合成的像元干净。
3.2 编写Sentinel-1预处理函数
定义一个函数,把S1原始值转成dB,并做好影像属性拷贝:
function prepS1(img) { var db = img.select(['VV', 'VH']) .log10() .multiply(10) .rename(['S1_VV', 'S1_VH']); return db .copyProperties(img, ['system:time_start', 'relativeOrbitNumber_start']) .set('datatype', 'dB'); }copyProperties是为了保留时间属性和轨道编号,因为后续做时间匹配时system:time_start非常重要。我把转换后的波段重命名为S1_VV和S1_VH,目的就是为了和后面S2的波段名S2_Blue这些形成一套清晰、不会撞车的新命名体系。
如果你需要更精细的辐射校正,可以在这一步再做入射角归一化。但先说明白:S1的后向散射受入射角影响很大,同一条轨道上,近距和远距的入射角可能差好几度。不同轨道的影像直接比较,会有系统性能量差异。我的做法是,当研究区跨度不大(比如小于一个S1条带的宽度),直接忽略入射角归一化;当研究区很大或要做长时间定量变化分析,就要用每个像元的入射角波段做回归校正。GEE数据集里自带angle波段,可以用它把后向散射基准归一到一个标准入射角。
3.3 编写Sentinel-2云掩膜函数
QA60的方法虽然简短,但我把它写完整,让第一次接触位运算的读者也能看懂:
function maskS2clouds(image) { var qa = image.select('QA60'); 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); }这段代码里,1 << 10是位运算中的左移,相当于二进制里的第10位被置为1,也就是代表云的位置。qa.bitwiseAnd(cloudBitMask).eq(0)表示“该位不是1”,也就是没有云。同理处理卷云。这样得到的掩膜,云和卷云区域都是0,其他区域是1,再updateMask进原影像,云就被剔掉了。
然后做一个波段选择和统一命名的函数:
function prepS2(img) { var s2 = img.select( ['B2', 'B3', 'B4', 'B8', 'B11', 'B12'], ['S2_Blue', 'S2_Green', 'S2_Red', 'S2_NIR', 'S2_SWIR1', 'S2_SWIR2'] ).resample('bilinear'); return s2.copyProperties(img, ['system:time_start']); }注意这里我对B11和B12做了resample('bilinear'),虽然它们还是20米网格,但已在元数据层面标记了双线性重采样方式,后续统一到10米网格时会更丝滑。
3.4 用Join做最近邻时间匹配与波段合并
现在进入正题,把S1和S2连接起来。先写Join方法,这个方法用起来更顺滑,因为它在一次操作里完成时间最近邻查找。
我用的思路是:对S2集合里的每一景影像,在限定的时间窗内找出时间差最小的那一景S1,然后把S1波段给它加上。
在GEE中,可以用ee.Join.saveAll加排序的方式。实测下来,这种方法比自己写循环要稳:
var s1ForJoin = s1Col.map(prepS1); var s2ForJoin = s2Col.map(prepS2); var timeDiffFilter = ee.Filter.maxDifference({ difference: 5 * 24 * 60 * 60 * 1000, leftField: 'system:time_start', rightField: 'system:time_start' }); var saveAllJoin = ee.Join.saveAll('matched_s1'); var joined = saveAllJoin.apply({ primary: s2ForJoin, secondary: s1ForJoin, condition: timeDiffFilter });这段代码的含义是:对每个S2影像,找到S1集合里所有与之时间差在5天以内的影像,把它们放进matched_s1这个属性里。5天是我在合成时序数据时比较常用的窗口,你完全可以根据自己研究区的重访密度把这个值调成3天或7天。
接下来,把每个S2影像从它匹配列表里挑出时间最接近的那一景S1:
function selectNearestS1(img) { var s1List = ee.List(img.get('matched_s1')); var s1ColTmp = ee.ImageCollection.fromImages(s1List); var s1Nearest = ee.Image(s1ColTmp .map(function(s1img) { var diff = ee.Date(s1img.get('system:time_start')) .difference(ee.Date(img.get('system:time_start')), 'millisecond') .abs(); return s1img.set('time_diff', diff); }) .sort('time_diff') .first()); return img.addBands(s1Nearest); } var matchedCol = joined.map(selectNearestS1);处理完成后,matchedCol里的每张影像,都同时拥有S2的光学波段和S1的雷达波段。这里有几个细节要说明:
- 中文用户经常会遇到的一个坑是:
ee.ImageCollection.fromImages要求list元素必须是ee.Image,如果你的matched_s1是空列表,first()会返回null,后续的addBands就会报错。所以我一般会在selectNearestS1里加一个判断:如果ee.List(img.get('matched_s1')).size()等于0,就直接返回只有S2波段的影像,或者丢弃这一景。 addBands(s1Nearest)没有指定名称时,会保留S1原来的波段名,也就是我在prepS1里重命名好的S1_VV、S1_VH,所以合并后的波段是S2_Blue, S2_Green, S2_Red, S2_NIR, S2_SWIR1, S2_SWIR2, S1_VV, S1_VH,一共8个波段。- 因为我在
prepS2和prepS1里都重命名了波段,所以不会出现同名波段被自动加_1后缀的情况。
如果担心Join用不熟,还有一套更朴素的窗口合成法:
var intervals = ee.List.sequence(0, 90, 10); var compositeCol = ee.ImageCollection(intervals.map(function(offset) { var start = ee.Date(startDate).advance(offset, 'day'); var end = start.advance(10, 'day'); var s1Comp = s1Col.filterDate(start, end).map(prepS1).median(); var s2Comp = s2Col.filterDate(start, end).map(prepS2).median(); var mergedImg = s2Comp.addBands(s1Comp) .set('system:time_start', start.millis()) .set('window_end', end); return mergedImg; }));这是“每10天一个窗口,窗口内取中值”的做法,对雷达斑点噪声的抑制效果很好,适合做分类任务。两套代码我都跑过,Join法输出的时间标签更真实,窗口法输出的数据更平滑,没有绝对的好坏,看你的应用场景。
3.5 可视化、导出与波段顺序确认
合并完之后,先别急着导出,第一件事就是可视化验证。GEE的Map.addLayer提供了一个很直观的诊断方式:
var visOptical = {bands: ['S2_Red', 'S2_Green', 'S2_Blue'], min: 0, max: 3000}; var visRadar = {bands: ['S1_VV', 'S1_VH'], min: -20, max: 0}; Map.centerObject(region, 11); Map.addLayer(matchedCol.first(), visOptical, 'Optical composite'); Map.addLayer(matchedCol.first(), visRadar, 'Radar composite');注意,S2_SR的表面反射率数值范围是0到10000左右,在真彩色显示时把max设成3000通常视觉效果好;S1的dB值一般在-20到0之间,min和max按这个设置即可。
确认肉眼看起来没有明显的配准错位或云区污染后,再导出:
Export.image.toDrive({ image: matchedCol, description: 'S1S2_matched_collection', folder: 'GEE_exports', crs: 'EPSG:32651', scale: 10, maxPixels: 1e10, fileFormat: 'GeoTIFF' });当你要导出一个影像集合时,GEE会把集合里每一张影像都按同样的坐标参考和比例尺导出。crs可以根据你的研究区选对应的UTM带。如果你不知道自己应该用哪个UTM带,一个笨但安全的办法是先用region.projection()看一下你底图或者边界文件的投影,然后选一个匹配的。
如果你的实际需求只需要一个时刻的合并影像,那么直接从matchedCol里first()一张出来导出就行。如果是要整年逐日或逐旬的时序数据,那就得导出一整批GeoTIFF,并且给每张影像的文件名里加上日期,否则本地会乱成一团。我常在description里加上影像时间,虽然麻烦一点,但之后查找头痛少很多。
4. 常见问题与排查技巧实录
4.1 时间匹配结果为空,或者匹配到空值
这是最常遇到的问题。明明S1和S2都有数据,为什么Join之后很多影像没有匹配到S1?原因多半是时间窗口太窄。S1和S2的重访周期不一样,你设5天窗口,可能有些区域正好在窗口里没有S1过境。
我的排查套路是:先单独打印matched_s1列表,看看是不是空的,再用ui.Chart.feature.histogram或Map.addLayer核对S1集合本身在时间轴上的分布,确认到底是不是没有数据。如果确实存在空档,把窗口从5天放宽到7天或10天。
还有一个细节:S2在filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20))之后,会丢掉很多云量大的影像,这会让Join的匹配基数变小。云雨季节的影像本来就少,如果又筛得太狠,匹配数会急剧下降。可选办法是放宽影像级云量到50%,把像素级掩膜作为主要去云手段,尽管会增加计算量,但有效匹配会明显上升。
4.2 addBands报错或者波段名出现后缀
addBands报错通常有两个原因:一个是波段数量不匹配,一个是波段名冲突。如果你在prepS1和prepS2里不重命名,S2里有B8,S1里没有B8,按理说也不冲突;但如果你用的S2波段名恰好和S1的VV、VH同名,那它们就会被强制重命名成VV_1和VH_1,后续用到波段名的代码就会失效。
所以我在最前面特意统一命名成S1_VV、S1_VH、S2_Blue这种模式。这样代码可读性好,也不容易踩到同名波段的坑。如果发现已经出现了_1后缀,多半是源数据波段没有重命名,回到prep函数里检查一遍rename是否生效。
4.3 计算超时或内存报错
GEE虽然号称云计算,但也不是无限资源。当你把研究区设得特别大、时间跨度又长,再加上逐像元的云掩膜和Join匹配,计算量会迅速膨胀。我在处理大范围区域时,经常看到Computation timed out或者User memory limit exceeded。
解决办法按优先级排序:先缩小研究区或者减少时间窗口做原型测试,比如先跑一个8公里缓冲区、一个月数据,确认代码无误;然后尽可能用median()或mean()做合成,用合成后的影像参与运算,而不是让整个影像集合进入复杂度高的分类器;最后在导出时控制maxPixels,因为GEE默认对超大导出的限制很严格,本地GeoTIFF没必要一次导个几十亿像素。
4.4 坐标和投影不一致导致错位
S1和S2的机器坐标在一定精度内是能对齐的,但在处理一个非常大的区域,尤其是跨UTM带时,投影不一致会导致几张影像肉眼可见地“拼不上”。我的做法是:在导出阶段用统一的crs强制投影,比如研究区跨两个UTM带时,优先选面积占比大的那带,或者用投影坐标系来表达研究区边界,这样合并结果在几何上是一致的。
如果在计算中间就有错位,我还会用region.transform()或reproject把S1强制转到S2使用的CRS上再做波段运算。但强烈建议只在最后一步做reproject,不要在中间反复投影,否则每投影一次,重采样就会带来一次精度损耗。
4.5 常见问题速查表
| 问题现象 | 常见原因 | 快速解决方向 |
|---|---|---|
| S1匹配为空 | 时间窗口太窄;S1极化筛选过严 | 放大窗口到7天或10天;检查极化条件 |
| S2云掩膜后全是黑 | QA60位掩码用错; 像素值范围不对 | 回看位运算逻辑;改用SCL波段测试 |
| 波段名带_1后缀 | 源数据波段未重命名就addBands | 统一修改波段名,再合并 |
| 导出时内存超限 | 区域过大、时间过长 | 缩小测试区域,用中值合成降算量 |
| S1和S2明显错位 | 投影、坐标基准不一致 | 导出时指定统一CRS和scale |
| 分类精度不升反降 | 云污染混入、噪声未被压制 | 检查云掩膜和S2影像质量;尝试更长窗口的中值合成 |
5. 一个小技巧与个人心得
最后分享一个我屡试不爽的习惯:在做S1和S2连接分析时,别急着把全部波段一股脑扔进分类器。先做波段重要性探索,比如跑一遍随机森林,或者在特征空间里做一次主成分分析,看看哪些波段对目标类别的区分度最高。很多时候你会发现,S1的VH极化噪声较大,但不代表它不重要;S2的SWIR波段在区分干湿植被时非常关键。不同区域、不同作物、不同季节,最优波段组合差异很大。与其迷信“全波段融合”,不如把连接分析当作一个特征工程工具箱,每次根据任务重新挑选最合适的组合。
我的个人体会是,GEE里最容易翻车的不是语法,而是对数据特性和物理意义理解不够。S1的雷达波段不是用来“看”的,是用来感知地表结构和水分的;S2的光学波段是需要你主动做云掩膜去“清洗”的。把预处理做扎实,时间窗口、投影、波段命名这三个基础打牢,后面的分类、回归、变化检测自然顺很多。如果你在实践里遇到什么奇怪的报错,不妨把波段属性和影像时间打印出来,逐层排查,大多数问题都不难定位。