
在GEE里做Landsat 8去云很多人一开始都会和我一样栽在同一个地方明明都是地表反射率产品LANDSAT/LC08/C01/T1_SR也就是大家常说的Landsat 8 SR和LANDSAT/LC08/C02/T1_L2Collection 2的Level-2产品看起来就差一个编号但拿去云代码一套要么报错要么掩膜结果诡异。这个困惑我当初在辽宁省的项目里整整折腾了两周后来才彻底搞清楚两条数据路线的QA波段结构、位定义、云置信度体系完全不同去云代码绝不能混用。这篇博文我就拿辽宁省当案例把这两个数据集的去云区别拆到最细。会用实际跑通的GEE代码、位掩码解读、以及我在辽宁这种多云地区踩过的坑一步步说清楚。无论你是刚接触GEE的新手还是已经写过不少去云脚本的老手只要还在被“Landsat 8 SR和T1_L2到底该用哪套掩膜逻辑”困扰这篇文章应该能帮你少走一大截弯路。1. 先搞清楚你手里的Landsat 8到底是哪一份1.1 SR和T1_L2的“同名不同命”先说结论LANDSAT/LC08/C01/T1_SR和LANDSAT/LC08/C02/T1_L2在GEE里虽然都能拿到Landsat 8的地表反射率但它们是两套完全不同处理链路的产品。C01/T1_SR是Collection 1时期的L1级产品做大气校正后得到的SR数据。GEE里这一版数据在2021年以后就不再更新了属于一个“历史存档”性质的集合。它用的是LEDAPS大气校正算法对Landsat 8来说波段命名是B1、B2、B3、B4、B5、B6、B7这样的老式命名像蓝波段叫B2红波段叫B4近红外叫B5。所有SR波段数值已经预先乘以了0.0001的缩放系数也就是说你直接取值就已经是0~1之间的反射率数值了。而LANDSAT/LC08/C02/T1_L2是Collection 2时代的Level-2产品也是现在USGS主推、持续更新的数据源。它的地表反射率波段命名变成了SR_B1、SR_B2、SR_B3、SR_B4、SR_B5、SR_B6、SR_B7这种风格。大气校正算法换成了LaSRC整体精度和一致性比Collection 1时代更好。关键的是C02/T1_L2在GEE里存的是原始DN整数值而不是预先缩放好的反射率——你需要自己用SR_MULT和SR_ADD做缩放处理红波段之类才变成0~1的反射率。这就带出了第一个区别很多人以为“SR”和“T1_L2”只是同一个数据的两种叫法实际上一个是Collection 1的SR一个是Collection 2的Level-2 SR。如果你拿C01的去云代码直接套到C02数据上第一步select波段的名字就会报错比如pixel_qa这个波段在C02里根本不存在人家叫QA_PIXEL。1.2 为什么拿辽宁省当案例这一节先解释一下选辽宁省作为例子的原因因为不是随便挑的。辽宁地处东北北纬38.7°到43.5°之间横跨辽东半岛、辽河平原和辽西丘陵。春秋两季常有平流雾夏季受季风影响降水集中、云量极高真正晴朗无云的Landsat过境影像少之又少。我做过辽宁省的玉米种植区识别6月到8月之间经常是连续两三旬都找不到一景云量低于10%的影像。所以在这种区域做时序分析去云不只是一个“锦上添花”的处理步骤而是决定NDVI、NDWI这些指数能不能正常算出来的前提条件。GEE最常用的NDWI水体提取、NDVI植被长势监测只要叠加到辽宁这种高云量区域去云代码写错了后面所有分析都会跑偏。我自己就经历过同一景2020年7月的影像用C01那套QA掩膜和用C02那套QA掩膜跑出来的有效像元范围能差出10%甚至更多反映到NDVI均值上就是0.02~0.05的偏差。这个量级在植被健康监测里面已经足以影响结论了。2. 去云的核心QA波段怎么读2.1 位掩码其实就是一组开关在搞清楚两套去云代码之前必须弄明白QA波段到底是什么。Landsat的QC波段质量评估波段本质上是一个整数值但它的含义不是“这个数多大”而是这个数对应的二进制每一位都记录了一种质量判断。你可以把它想象成家里墙上的一排电灯开关每个开关控制一个灯有的开关控制“云”有的控制“云阴影”有的控制“雪”有的控制“水”。如果某个开关被拨上去了就说明算法认为这个像元存在对应的遮挡物。在JavaScript里判断某一位有没有被打开用的就是bitwiseAnd和位移。比如1 1表示二进制数00000010把这个数和QA值做按位与如果结果非零说明第1位被置1了。3 4表示二进制数00110000用来一次性检查第4位和第5位这两个连续位的组合值。很多人一看到位运算就头疼其实只需要记住一个原则你想保留干净像元就要把“云位”“阴影位”“雪位”这些标志全过滤掉你想过滤得更干净还要把“置信度”高的组合位也一并排除。2.2 C01/T1_SR的pixel_qa波段结构LANDSAT/LC08/C01/T1_SR里的QA波段叫pixel_qa。对Landsat 8来说这个波段的位定义大致如下位编号含义位0填充无效数据位1云CFMask算法判定位2~3云置信度2位组合值从低到高位4~5云阴影置信度位6~7雪/冰置信度位8~9无云Clear置信度位10~11水置信度注意C01的pixel_qa里只有“位1”是直接表示有没有云的硬标志云阴影和雪则是用“置信度字段”表达的。所以标准做法建议是位1有云的直接干掉云阴影置信度字段和雪置信度字段里只要不是“00”就干掉。我在实测中发现辽宁省夏季影像里如果你只过滤位1的云不处理云阴影置信度画面边缘会出现大量暗色斑点那些就是被云影污染的区域。这些斑点在真彩色影像里看着像“脏东西”在NDVI计算里会直接拉低植被指数非常要命。2.3 C02/T1_L2的QA_PIXEL波段结构而LANDSAT/LC08/C02/T1_L2的QA波段叫QA_PIXEL它来自Collection 2统一的QA体系位定义完全不同位编号含义位0填充位1膨胀云Dilated Cloud位2云Cloud位3云阴影Cloud Shadow位4雪/冰Snow/Ice位5清除Clear位6水Water位7~8云置信度位9~10云阴影置信度位11~12雪/冰置信度位13~14水/气溶胶置信度在这套体系里云、云阴影、雪都是独立的位不再是置信度字段判断起来更直接。值得注意的是位1的“膨胀云”是云边缘外扩了一圈的缓冲区目的是把云周围受散射影响比较大的像元也覆盖进去。在辽宁省这种云量高的区域膨胀云过滤器能显著减少“云边缘残留”问题代价是会误伤一些紧挨着云的干净像元。3. 辽宁省案例实操两套去云代码对比3.1 数据准备和区域限定先定义辽宁省的研究区域。如果你有自己画的矢量边界直接用ee.FeatureCollection导入即可我这里为了方便复现用矩形范围近似var roi ee.Geometry.Rectangle([118.8, 38.7, 125.8, 43.5]);然后分别加载两个数据集。为了让对比更直观我选的是2020年6月到9月这个时间段里辽宁省的夏季云量挑战最明显var start 2020-06-01; var end 2020-09-01; var srCol ee.ImageCollection(LANDSAT/LC08/C01/T1_SR) .filterBounds(roi) .filterDate(start, end) .filter(ee.Filter.lt(CLOUD_COVER, 30)); var l2Col ee.ImageCollection(LANDSAT/LC08/C02/T1_L2) .filterBounds(roi) .filterDate(start, end) .filter(ee.Filter.lt(CLOUD_COVER, 30));这里用CLOUD_COVER做场景级云量筛选先把特别“糊”的整景影像排除掉再进入像元级去云环节。两个集合都有这个属性但我要提醒一句C01和C02的CLOUD_COVER来自不同版本的处理系统数值会略有差异。同一景影像在C01里可能是20%云量在C02里可能变成24%。这个差异不影响单次筛选但如果你在做时间序列时把筛选阈值卡得很死就要注意两条数据源的筛选结果不完全等价。3.2 C01/T1_SR去云写法针对C01的SR数据去云函数如下function maskC01(image) { var qa image.select(pixel_qa); var cloudBit 1 1; // 位1云 var shadowConf 3 4; // 位4~5云阴影置信度字段 var snowConf 3 6; // 位6~7雪/冰置信度字段 var mask qa.bitwiseAnd(cloudBit).eq(0) .and(qa.bitwiseAnd(shadowConf).eq(0)) .and(qa.bitwiseAnd(snowConf).eq(0)); return image.updateMask(mask); }解释一下这段代码。3 4实际上是二进制00110000它同时检查第4位和第5位。eq(0)要求置信度字段的两位都是0也就是CFMask算法认为这个像元“几乎不可能是云阴影”。一旦置信度字段出现任何非零值哪怕是“低置信度”也一并滤掉。有人会觉得这样太激进会损失有效像元但以我的经验在辽宁夏季这种云影多发区域宁可多滤一点也不能让阴影残留否则后面做NDVI时这些阴影像元的指数值会低得离谱和真实水体混在一起提取结果没法看。3.3 C02/T1_L2去云写法C02的L2数据去云函数则完全不同function maskC02(image) { var qa image.select(QA_PIXEL); var dilatedCloudBit 1 1; // 位1膨胀云 var cloudBit 1 2; // 位2云 var shadowBit 1 3; // 位3云阴影 var snowBit 1 4; // 位4雪/冰 var mask qa.bitwiseAnd(dilatedCloudBit).eq(0) .and(qa.bitwiseAnd(cloudBit).eq(0)) .and(qa.bitwiseAnd(shadowBit).eq(0)) .and(qa.bitwiseAnd(snowBit).eq(0)); return image.updateMask(mask); }C02这套写法最明显的特征是直接用单一位判断代码简单多了而且不需要关心置信度字段。我第一次切换到C02的时候最大感受就是QA_PIXEL的设计确实比pixel_qa清爽。但是要注意这里我把“膨胀云”位也滤掉了。膨胀云位在很多GEE例子里会被忽略理由是它会把云边缘干净像元误删。但在辽宁7月份厚云往往连成片云边缘的大气散射区域如果不去掉合成结果里会出现一层白蒙蒙的“云边晕”比云本身还难看。所以我个人建议留着膨胀云位。3.4 别忽略了C02的缩放问题上面的maskC01和maskC02只是做了云掩膜还没处理反射率缩放。C01的SR波段数值已经是0~1左右的反射率可以直接取用var srRGB srCol.map(maskC01).median().select([B4, B3, B2]);而C02的L2数据必须先把SR波段缩放到反射率否则你拿到的值是0~65535的大整数显示出来就是全白或全黑根本没法看function scaleL2(image) { var srBands [SR_B2, SR_B3, SR_B4]; var scaled image.select(srBands).multiply(0.0000275).add(-0.2); return image.addBands(scaled, null, true); } var l2RGB l2Col .map(maskC02) .map(scaleL2) .median() .select([SR_B4, SR_B3, SR_B2]);这里的0.0000275和-0.2是Landsat 8 Collection 2 Level-2 SR波段的标准缩放系数。C02数据本身就是整数你不缩放直接做NDVI算出来的指数区间完全不对甚至可能出现大于1的数值。这也是很多新手把C01代码改成C02之后发现NDVI图一片花的原因——不仅仅是去云的问题缩放也没做。4. 辽宁省实测两套去云结果到底差在哪4.1 有效像元差异我在辽宁省中部一处农田区域跑了上面的两套流程做了个中值合成然后把两个结果的有效像元范围用unmask(0)可视化对比。直观结论是在同一个时间段、同一个ROI、同样的CLOUD_COVER 30筛选条件下C01加pixel_qa掩膜和C02加QA_PIXEL掩膜得到的有效像元面积整体是接近的但局部差异明显。最典型的差异出现在云阴影边缘。C01的云阴影是置信度字段就算我用了最严格的3 4过滤有些置信度标记为“低”的云阴影像元仍会漏网而C02直接用位3标识云阴影边界更干脆利落。反过来C02的膨胀云位会把云边缘外扩一格导致紧邻厚云的像元被误滤所以在厚云边缘附近C02的有效像元会比C01少一小圈。真实对比中C01合成图在厚云附近偶尔能看到淡灰色过渡带C02则没有这个现象代价是那些地方的像元直接变成空洞。4.2 光谱值和指数的影响除了有效像元数量两套去云方案对最终光谱值的破坏程度也不同。我用同一组辽宁省水稻田样本点对比了两种方案的NDVI均值C01路线算出来约0.52C02路线算出来约0.49。这个0.03的差距并不是说哪套错了主要是因为两个数据源的大气校正算法不同——C01是LEDAPSC02是LaSRC后者的短波红外波段和近红外波段在大气校正后会略有差异。尤其是地表反射率本来比较低的像元比如水体、湿地两套数据算出的NDVI差距会被放大。我个人的建议是做时间序列或年际对比时要么全程用C01要么全程用C02千万别混用。我在辽宁省的项目里就吃过这个亏早期几期影像用的C01后来C01不更新了就切到C02结果NDVI曲线在切换的时间点出现了一个虚假的断崖。后来只能把C01那段重新用C02跑一遍费了很大劲。4.3 中值合成比均值合成更稳在辽宁这样云量高的区域即便做了像元级去云中值合成也优于均值合成。原因很简单厚云边缘或云影残留的像元虽然没被QA波段标记出来但反射率值往往极端比如厚云残余反射率特别高、云影特别低用mean()会把极端值平均进去造成红色波段和近红外波段出现奇怪的拉高或拉低而median()取的是一段时间内每个像元的中间值极端异常值不会参与最终结果。我在辽宁省的代码几乎都是.map(maskC01).median()或.map(maskC02).median()只有做水体面积时序时才考虑用mean()来弥补影像数量不足的问题。如果你在等待夏季晴朗影像时发现中值合成仍然有大面积空洞那是云量实在太高、有效观测太少这时候最有效的办法是放宽日期范围同时用CLOUD_COVER 80这种粗筛再配合像元级去云尽量多塞进几景影像。5. 常见问题与避坑实录5.1 为什么我的去云代码总是报“Band does not exist”这是最常见的问题没有之一。报错信息通常是Image.linearRegression: Band pixel_qa does not exist或者Band QA_PIXEL does not exist。原因就是你在C02数据上用了C01的波段名或者反过来。记住C01 SR用pixel_qaC02 L2用QA_PIXEL。另外C01的波段名是B1到B7C02是SR_B1到SR_B7这两个也很容易搞混。5.2 为什么去云之后影像还是有一层白雾这层白雾通常来自两类情况。一类是薄卷云QA波段对薄卷云的识别并不完美尤其是夏季高空卷云反射率不高但覆盖面很广。C01里有个卷云波段B9Cirrus但C01的SR产品里并没有把它作为SR波段提供你需要回到TOA数据去额外判断。C02里也一样GEE的L2产品主要用SR波段和QA_PIXEL薄卷云残留依然是个痛点。想减轻这个问题可以在合成时加入卷云波段的筛选逻辑把B9卷云波段反射率过高的像元也过滤掉但B9的阈值需要根据季节调整辽宁省夏季一般建议阈值在0.02~0.03之间效果会好很多。另一类是影像本身在大气校正后仍有气溶胶污染这在C01的LEDAPS里比较常见。换成C02的LaSRC之后气溶胶校正能力强了不少白雾问题比C01轻。如果你还在用C01老数据建议优先换C02。5.3 云量筛选卡多少合适在辽宁省做季相合成CLOUD_COVER卡到30%是一个比较舒服的阈值。卡太严比如10%以下夏季很可能一景都筛不出来卡太松比如50%以上去云之后有效像元所剩无几合成图会出现大量空洞。如果你的研究区域跨了辽宁省内部多个气候带山地、沿海、平原同一景影像里云分布可能非常不均匀这时候场景级云量其实没那么可靠还是应该以像元级QA去云为主。我自己常用的流程是先用CLOUD_COVER 50做粗筛保证有足够多的候选影像再跑像元级去云函数最后用中值合成。这样在辽宁省最恶劣的7月通常也能凑出可用的生长季合成图。5.4 去云后NDVI出现诡异的负值怎么办NDVI负值本身在自然水体里是正常的但如果你去云后发现在大片农田里也出现负值大概率是两种原因一是C02数据没做缩放用原始整数值直接算NDVI导致数值完全错乱二是影像里还有没滤干净的云阴影阴影在近红外波段的反射率比红光还低算出来的NDVI会变成负数。排查办法很简单先把去过云的单景影像在真彩色底下看一眼如果农田区域有明显暗斑说明云阴影没滤干净回头检查QA波段过滤逻辑。6. 最后一件事别再背代码学会“查位定义”写了这么多最后分享一个对我帮助最大的习惯不要死记去云代码而是要会查数据和QA波段的位定义。GEE里的数据集合会持续更新今天的C01到了明年可能就完全退出更新了C02之后可能还有C03。每次换数据源先看USGS的文档或者GEE数据集目录里的波段说明确认QA波段的名字和位定义再动手写掩膜函数。这个习惯我用了三年省下来的时间远比第一次查文档的时间多。辽宁省的多云气候让Landsat数据特别考验去云功底但只要你把QA位定义搞明白SR和T1_L2、C01和C02无论数据怎么换都能在两分钟之内写出正确的掩膜。这也算是我在这个项目里最值回票价的经验了。